using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Principal;

namespace BilSimser.SharePoint.Common.Service
{
	/// <summary>
	/// This class manages the creation of an impersonation to the Application
	/// Pool's context for a defined scope.
	///
	/// It just wraps the .Net class for impersonation, thus allowing easier
	/// use.
	///
	/// This class is made for use with the C# 'using' keyword, as in the
	/// example below:
	/// <code>
	/// using(Identity impersonate = Identity.ImpersonateAdmin())
	/// {
	///		// ... Do whatever job you want as the AppPool
	/// }
	/// // Back to the original, you don't have to worry about reverting,
	/// // this is automatically done with the IDisposable interface
	/// </code>
	/// </summary>
	public sealed class Identity : IDisposable
	{
		/// <summary>
		/// Windows identity used for the Application Pool
		/// </summary>
		private static WindowsIdentity appPoolIdentity;

		/// <summary>
		/// Gets the windows identity used for the Application Pool
		/// </summary>
		private static WindowsIdentity AppPoolIdentity
		{
			get
			{
				// Lock current type to ensure thread safety on
				// identity creation.
				lock (typeof (Identity))
				{
					if (appPoolIdentity == null)
					{
						// Create a new handle from this one
						IntPtr token = WindowsIdentity.GetCurrent().Token;

						// Throw an exception if we have an empty token
						if (token == IntPtr.Zero)
						{
							throw new ApplicationException("Unable to fetch AppPool's identity token !");
						}

						// Create a duplicate of the user's token in order to use it for impersonation
						if (! DuplicateToken(token, 2, ref token))
						{
							throw new Win32Exception(Marshal.GetLastWin32Error(), "Unable to duplicate AppPool's identity token !");
						}

						// Throw an exception if we were unable to duplicate the token
						if (token == IntPtr.Zero)
						{
							throw new ApplicationException("Unable to duplicate AppPool's identity token !");
						}

						// Store app pool's identity
						appPoolIdentity = new WindowsIdentity(token);

						// Free the windows unmanaged resource
						CloseHandle(token);
					}
					return appPoolIdentity;
				}
			}
		}

		/// <summary>
		/// This function returns the current user's login name.
		/// </summary>
		public static string CurrentUserName
		{
			get { return WindowsIdentity.GetCurrent().Name; }
		}

		/// <summary>
		/// Stores the currently available Windows Impersonation context.
		/// </summary>
		private WindowsImpersonationContext context;

		/// <summary>
		/// Stores the app pool's identity context.
		/// </summary>
		private WindowsImpersonationContext selfContext;

		/// <summary>
		/// Private constructor, static function accessed class.
		/// </summary>
		private Identity()
		{
			// Try catch structure to ensure we don't change context in case
			// we had an error duplicating the token.
			try
			{
				selfContext = WindowsIdentity.Impersonate(IntPtr.Zero); // REVERT to AppPool identity!
				context = AppPoolIdentity.Impersonate();
			}
			catch
			{
				// Close the context
				UndoImpersonation();

				// Rethrow the exception
				throw;
			}
		}

		/// <summary>
		/// This method creates a new impersonation context.
		/// </summary>
		public static Identity ImpersonateAppPool()
		{
			return new Identity();
		}

		/// <summary>
		/// This method closes the current impersonation context in order revert the user
		/// to his real principal.
		/// </summary>
		public void UndoImpersonation()
		{
			if (context != null)
			{
				context.Undo();
				context = null;
			}
			if (selfContext != null)
			{
				selfContext.Undo();
				selfContext = null;
			}
		}

		/// <summary>
		/// Duplicates a token in order to have it working for impersonation.
		/// </summary>
		/// <param name="hToken_">Initial token to be duplicated</param>
		/// <param name="impersonationLevel_">Level of impersonation needed</param>
		/// <param name="hNewToken_">Reference to the new token created</param>
		/// <returns>True if the call succeeded, false otherwise.</returns>
		[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
		private static extern bool DuplicateToken(IntPtr hToken_, int impersonationLevel_, ref IntPtr hNewToken_);

		/// <summary>
		/// Closes an unmanaged handle in order to free allocated resources.
		/// </summary>
		/// <returns>True if the call succeeded, false otherwise.</returns>
		[DllImport("kernel32.dll", CharSet=CharSet.Auto)]
		private static extern bool CloseHandle(IntPtr handle);

		#region IDisposable Members

		/// <summary>
		/// This method disposes the current object, it frees all resources used by this class.
		/// </summary>
		public void Dispose()
		{
			Dispose(true);
			// Ensure I'm garbage collected.
			GC.SuppressFinalize(this);
		}

		/// <summary>
		/// This method disposes the current object, it frees all resources used by this class.
		/// </summary>
		/// <param name="disposing_">Do actual disposing or not.</param>
		private void Dispose(bool disposing_)
		{
			if (disposing_)
			{
				this.UndoImpersonation();
			}
		}

		#endregion
	}
}